﻿using BrightIdeasSoftware;
using StableDiffusionTools.Forms;
using StableDiffusionTools.Framework;
using StableDiffusionTools.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using TaggingToolkit.Framework;
using TaggingToolkit.Controls;
using StableDiffusionTools.AUTOMATIC1111;
using System.Drawing;
using ComponentFactory.Krypton.Toolkit;
using StableDiffusionTools.Enums;
using StableDiffusionTools.Themes;
using StableDiffusionTools.Controls.KryptonHelper;
using TaggingToolkit.Events;
using StableDiffusionTools.Events;
using System.Globalization;
using System.Drawing.Imaging;
using StableDiffusionTools.Imaging;
using StableDiffusionTools.Interop;

namespace TaggingToolkit.Forms
{
    [Export(typeof(IForm))]
    [ExportMetadata("Name", "Tagging Toolkit")]
    [ExportMetadata("Image", "toolbox")]
    public partial class TaggingToolkitForm : KryptonForm, IForm, IMessageFilter
    {
        const float lanczocRadius = 3.0f;

        public event EventHandler<BusyChangedEventArgs> BusyChanged;
        protected void OnBusyChanged()
        {
            BusyChanged?.Invoke(this, new BusyChangedEventArgs(busy));
        }

        private bool busy = false;
        public bool Busy
        {
            get => busy;
            protected set
            {
                if (value != busy)
                {
                    busy = value;
                    OnBusyChanged();
                }
            }
        }

        private SettingsManager<TaggingToolkitSettings> settingsMan = new SettingsManager<TaggingToolkitSettings>(nameof(TaggingToolkitSettings), createBackups: false);
        private TypedObjectListView<DataTags> tolvTags;
        private TextMatchFilter filter;
        private TaggingToolkitSettings settings;
        private TagSearchReplaceForm tagSearchReplaceForm;
        private SettingsForm settingsForm;

        //private InterrogationState interrogation = new InterrogationState();
        private ThreadedThumbnailLoader thumbnailLoader;
        private ThreadedInterrogator interrogator;
        private bool hasDirtyState = false;
        private bool closing = false;
        private bool displayImagePreviews = false;
        private ClipType clipType;
        private bool ignoreCropIndexChanged = false;
        private bool ignoreFinalIndexChanged = false;
        private bool didCrop = false;

        private readonly List<char[]> ClipSplit = new List<char[]>() { new char[] { ',', ' ' }, new char[] { ',' } };

        public bool PreFilterMessage(ref Message m)
        {
            if (imageControl1.Capture && m.Msg == (int)WindowMessage.MouseWheel)
            {
                User32.SendMessage(imageControl1.Handle, WindowMessage.MouseWheel, m.WParam, m.LParam);
                return true;
            }

            return false;
        }

        public TaggingToolkitForm()
        {
            InitializeComponent();
            GlobalTheme.Init();
            buttonSpecTheme.KryptonContextMenu = new KryptonThemeContextMenu();

            Application.AddMessageFilter(this);

            Icon = StableDiffusionTools.Properties.Resources.toolbox1;
            tsbBrowse.Image = StableDiffusionTools.Properties.Resources.folder16;
            tsbLoad.Image = StableDiffusionTools.Properties.Resources.document_import16;
            tsbSave.Image = StableDiffusionTools.Properties.Resources.document_export16;
            tsddActions.Image = StableDiffusionTools.Properties.Resources.action16;
            ((ToolStripDropDownMenu)tsddActions.DropDown).ShowCheckMargin = false;
            countTagsToolStripMenuItem.Image = StableDiffusionTools.Properties.Resources.calculator_add16;
            tsbEditTags.Image = StableDiffusionTools.Properties.Resources.bullet_wrench16;
            tsbSearchAndReplace.Image = StableDiffusionTools.Properties.Resources.text_replace16;
            tsddbMark.Image = StableDiffusionTools.Properties.Resources.pencil16;
            ((ToolStripDropDownMenu)tsddbMark.DropDown).ShowCheckMargin = false;
            markToolStripMenuItem.Image = StableDiffusionTools.Properties.Resources.pencil_add16;
            clearToolStripMenuItem.Image = StableDiffusionTools.Properties.Resources.pencil_delete16;
            tsbSettings.Image = StableDiffusionTools.Properties.Resources.gear16;

            openToolStripMenuItem.Image = StableDiffusionTools.Properties.Resources.open16;
            openExplorerToolStripMenuItem.Image = StableDiffusionTools.Properties.Resources.openfolder16;
            openTagEditorToolStripMenuItem.Image = StableDiffusionTools.Properties.Resources.bullet_wrench16;
            interrogateToolStripMenuItem.Image = StableDiffusionTools.Properties.Resources.tag_green16;

            settings = settingsMan.LoadSettings();
            settings.MaxFileReadThreads = Math.Max(settings.MaxFileReadThreads, 1);
            settings.MaxWorkerThreads = Math.Max(settings.MaxWorkerThreads, 1);

            WebUIAPI.WebUIPort = settings.WebUIPort;

            void imageControlMouseLeave(object s, EventArgs e)
            {
                if (!imageControl1.Bounds.Contains(imageControl1.PointToClient(MousePosition)))
                {
                    if (kryptonCHKBTNLetterbox.Focused || kryptonCMBCropSize.Focused || kryptonCHKBTNFinalSize.Focused || kryptonCMBFinalSize.Focused || kryptonBTNApplyCrop.Focused)
                        imageControl1.Focus();

                    kryptonBTNApplyCrop.Visible = kryptonCHKBTNLetterbox.Visible = kryptonCMBCropSize.Visible = kryptonCHKBTNFinalSize.Visible = kryptonCMBFinalSize.Visible = false;
                }
            };

            //Letterbox Sizes
            int selectedIndexLetterbox = -1;
            for (int i = 0; i < settings.LetterboxSizes.Count; i++)
            {
                kryptonCMBCropSize.Items.Add($"{settings.LetterboxSizes[i].Width}x{settings.LetterboxSizes[i].Height}");
                if (settings.LastLetterboxSelection.Equals(settings.LetterboxSizes[i]))
                {
                    selectedIndexLetterbox = i;
                }
            }
            kryptonCMBCropSize.SelectedIndex = selectedIndexLetterbox;
            kryptonCMBCropSize.SelectedIndexChanged += (s, e) =>
            {
                if (ignoreCropIndexChanged)
                    return;

                if (kryptonCMBCropSize.SelectedIndex >= 0)
                    imageControl1.SetLetterBoxBounds(new RectangleF(imageControl1.LetterboxBounds.Location, settings.LetterboxSizes[kryptonCMBCropSize.SelectedIndex]));
                else
                    imageControl1.SetLetterBoxBounds(new RectangleF(), false);
            };

            kryptonCMBCropSize.MouseLeave += imageControlMouseLeave;

            kryptonCMBCropSize.KeyDown += (s, e) =>
            {
                if(e.KeyCode == Keys.Enter)
                {
                    if (SizeUtil.TryParse(kryptonCMBCropSize.Text, out Size size) )
                    {
                        if (!kryptonCMBCropSize.Items.Contains($"{size.Width}x{size.Height}"))
                        {
                            settings.LetterboxSizes.Add(size);
                            int newIndex = kryptonCMBCropSize.Items.Add($"{size.Width}x{size.Height}");
                            kryptonCMBCropSize.SelectedIndex = newIndex;
                        }
                    }
                }
            };

            kryptonCTXLetterboxSizes.Opening += (s, e) =>
            {
                if (SizeUtil.TryParse(kryptonCMBCropSize.Text, out Size size))
                {
                    bool containsSize = settings.LetterboxSizes.Contains(size);
                    kryptonContextMenuItem1.Enabled = !containsSize;
                    kryptonContextMenuItem2.Enabled = containsSize;
                }
                else
                {
                    kryptonContextMenuItem1.Enabled = kryptonContextMenuItem2.Enabled = false;
                }
            };

            kryptonCTXLetterboxSizes.Closed += (s, e) => imageControlMouseLeave(s, EventArgs.Empty);
            //Add
            kryptonContextMenuItem1.Click += (s, e) =>
            {
                if (SizeUtil.TryParse(kryptonCMBCropSize.Text, out Size size))
                {
                    settings.LetterboxSizes.Add(size);
                    int newIndex = kryptonCMBCropSize.Items.Add($"{size.Width}x{size.Height}");
                    kryptonCMBCropSize.SelectedIndex = newIndex;
                }
            };

            //Remove
            kryptonContextMenuItem2.Click += (s, e) =>
            {
                if (SizeUtil.TryParse(kryptonCMBCropSize.Text, out Size size))
                {
                    int itemIndex = kryptonCMBCropSize.Items.IndexOf($"{size.Width}x{size.Height}");
                    if (itemIndex != -1)
                    {
                        if (kryptonCMBCropSize.Items.Count > 1)
                            kryptonCMBCropSize.SelectedIndex = MathUtil.Clamp(itemIndex > 0 ? itemIndex - 1 : itemIndex + 1, -1, kryptonCMBCropSize.Items.Count - 1);
                        else
                        {
                            kryptonCMBCropSize.Text = "";
                            imageControl1.SetLetterBoxBounds(new RectangleF(), false);
                        }

                        kryptonCMBCropSize.Items.RemoveAt(itemIndex);
                        settings.LetterboxSizes.Remove(size);
                    }
                }
            };

            //Final Sizes
            int selectedIndexFinalSize = -1;
            for (int i = 0; i < settings.FinalSizes.Count; i++)
            {
                kryptonCMBFinalSize.Items.Add($"{settings.FinalSizes[i].Width}x{settings.FinalSizes[i].Height}");
                if (settings.LastFinalSizeSelection.Equals(settings.FinalSizes[i]))
                {
                    selectedIndexFinalSize = i;
                }
            }
            kryptonCMBFinalSize.SelectedIndex = selectedIndexFinalSize;
            kryptonCMBCropSize.SelectedIndexChanged += (s, e) =>
            {
                if (ignoreFinalIndexChanged)
                    return;

            };

            kryptonCMBFinalSize.MouseLeave += imageControlMouseLeave;

            kryptonCMBFinalSize.KeyDown += (s, e) =>
            {
                if (e.KeyCode == Keys.Enter)
                {
                    if (SizeUtil.TryParse(kryptonCMBFinalSize.Text, out Size size))
                    {
                        if (!kryptonCMBFinalSize.Items.Contains($"{size.Width}x{size.Height}"))
                        {
                            settings.FinalSizes.Add(size);
                            int newIndex = kryptonCMBFinalSize.Items.Add($"{size.Width}x{size.Height}");
                            kryptonCMBFinalSize.SelectedIndex = newIndex;
                        }
                    }
                }
            };

            kryptonCTXFinalSizes.Opening += (s, e) =>
            {
                if (SizeUtil.TryParse(kryptonCMBFinalSize.Text, out Size size))
                {
                    bool containsSize = settings.FinalSizes.Contains(size);
                    kryptonContextMenuItem3.Enabled = !containsSize;
                    kryptonContextMenuItem4.Enabled = containsSize;
                }
                else
                {
                    kryptonContextMenuItem3.Enabled = kryptonContextMenuItem4.Enabled = false;
                }
            };

            kryptonCTXFinalSizes.Closed += (s, e) => imageControlMouseLeave(s, EventArgs.Empty);
            //Add
            kryptonContextMenuItem3.Click += (s, e) =>
            {
                if (SizeUtil.TryParse(kryptonCMBFinalSize.Text, out Size size))
                {
                    settings.FinalSizes.Add(size);
                    int newIndex = kryptonCMBFinalSize.Items.Add($"{size.Width}x{size.Height}");
                    kryptonCMBFinalSize.SelectedIndex = newIndex;
                }
            };

            //Remove
            kryptonContextMenuItem4.Click += (s, e) =>
            {
                if (SizeUtil.TryParse(kryptonCMBFinalSize.Text, out Size size))
                {
                    int itemIndex = kryptonCMBFinalSize.Items.IndexOf($"{size.Width}x{size.Height}");
                    if (itemIndex != -1)
                    {
                        if (kryptonCMBFinalSize.Items.Count > 1)
                            kryptonCMBFinalSize.SelectedIndex = MathUtil.Clamp(itemIndex > 0 ? itemIndex - 1 : itemIndex + 1, -1, kryptonCMBFinalSize.Items.Count - 1);
                        else
                        {
                            kryptonCMBFinalSize.Text = "";
                            //imageControl1.SetLetterBoxBounds(new RectangleF(), false);
                        }

                        kryptonCMBFinalSize.Items.RemoveAt(itemIndex);
                        settings.FinalSizes.Remove(size);
                    }
                }
            };

            kryptonCHKBTNLetterbox.MouseLeave += imageControlMouseLeave;
            kryptonCHKBTNFinalSize.MouseLeave += imageControlMouseLeave;
            kryptonBTNApplyCrop.MouseLeave += imageControlMouseLeave;
            kryptonBTNApplyCrop.Click += (s, e) =>
            {
                CropSelectedItem();
            };

            imageControl1.Zoom = settings.LastZoom;
            imageControl1.SizeMode = settings.FitSize ? StableDiffusionTools.Controls.ImageSizeMode.Fit : StableDiffusionTools.Controls.ImageSizeMode.Zoom;
            imageControl1.SetLetterBoxBounds(new RectangleF(new PointF(), settings.LetterboxSizes.ElementAtOrDefault(selectedIndexLetterbox)), true);
            SetImageControlLetterboxColor(settings.LetterboxColor);
            imageControl1.MouseLeave += imageControlMouseLeave;
            imageControl1.KeyDown += (s, e) =>
            {
                if (e.KeyCode == Keys.Enter)
                {
                    CropSelectedItem();
                }
            };

            imageControl1.MouseWheel += (s, e) =>
            {
                if (ModifierKeys.HasFlag(Keys.Control))
                {
                    imageControl1.Focus();
                    ((HandledMouseEventArgs)e).Handled = true;

                    if (!kryptonCHKBTNLetterbox.Checked)
                        kryptonCHKBTNLetterbox.Checked = true;

                    int totalItems = kryptonCMBCropSize.Items.Count;
                    if (totalItems <= 0)
                        return;

                    int delta = e.Delta / 120;
                    kryptonCMBCropSize.SelectedIndex = MathUtil.Clamp(kryptonCMBCropSize.SelectedIndex - delta, 0, totalItems - 1);
                }
                if (ModifierKeys.HasFlag(Keys.Shift))
                {
                    imageControl1.Focus();
                    ((HandledMouseEventArgs)e).Handled = true;

                    if (!kryptonCHKBTNLetterbox.Checked)
                        kryptonCHKBTNLetterbox.Checked = true;

                    if (!kryptonCHKBTNFinalSize.Checked)
                        kryptonCHKBTNFinalSize.Checked = true;

                    int totalItems = kryptonCMBFinalSize.Items.Count;
                    if (totalItems <= 0)
                        return;

                    int delta = e.Delta / 120;
                    kryptonCMBFinalSize.SelectedIndex = MathUtil.Clamp(kryptonCMBFinalSize.SelectedIndex - delta, 0, totalItems - 1);
                }
            };

            imageControl1.MouseDown += (s, e) =>
            {
                if (e.Button == MouseButtons.Middle && !kryptonCHKBTNLetterbox.Checked)
                    kryptonCHKBTNLetterbox.Checked = true;
            };

            tolvTags = new TypedObjectListView<DataTags>(olvTags);

            thumbnailLoader = new ThreadedThumbnailLoader();
            SetThumbnailLoaderMaxThreadCount(settings.MaxFileReadThreads);
            thumbnailLoader.LoadedThumbnail += (s, e) =>
            {
                imageListSmall.Images.Add(e.Tags.ImagePath, e.Image);
                olvTags.RefreshObject(e.Tags);
            };

            int error = 0;
            int completed = 0;
            interrogator = new ThreadedInterrogator() { MaxThreads = settings.MaxWorkerThreads };
            interrogator.InterrogatedTag += (s, e) =>
            {
                e.Tag.ScheduledForInterrogation = false;

                if (e.Success)
                {
                    if (e.Tag.SetTagData(e.Caption))
                    {
                        hasDirtyState = true;
                        if (e.Tag == tolvTags.SelectedObject)
                        {
                            SetTagEditFromTag(e.Tag);
                        }
                    }
                }
                else
                {
                    if (e.ErrorSource == InterrogatedTagEventErrorSource.WebRequest)
                    {
                        SetStatusStripStatus(e.Error, warning: true);
                        if (++error >= 5)
                        {
                            interrogator.Stop();
                        }
                    }
                }
                tspbProgressEx.DisplayValue = $"{Interlocked.Increment(ref completed)}/{e.Total}";
                tspbProgressEx.Value = Math.Min(100, (int)((float)completed / e.Total * 100));
                olvTags.RefreshObject(e.Tag);
            };

            interrogator.InterrogatorIdle += (s, e) =>
            {
                tspbProgressEx.DisplayValue = "";
                tspbProgressEx.Value = 100;
                if (error == 0)
                {
                    SetStatusStripStatus("Interrogator Done");
                }
                error = 0;
                completed = 0;
                SetEnabledState(true);
            };

            var cFile = tolvTags.GetColumn("File");
            cFile.AspectGetter += (o) => o?.RelativePath;
            cFile.ImageGetter += (o) =>
            {
                string key = o?.ImagePath;
                if (!displayImagePreviews || string.IsNullOrEmpty(key))
                    return null;

                if(!thumbnailLoader.HasBeenScheduled(o))
                    thumbnailLoader.ScheduleForLoad(o, imageListSmall.ImageSize);

                return key;
            };
            olvColumnFile.Renderer = new ColoredTextRendererDataTags() { UseGdiTextRendering = false };

            var cTags = tolvTags.GetColumn("Tags");
            cTags.AspectGetter += (o) => o?.TagData;
            cTags.AspectPutter += (o, e) =>
            {
                if (!Busy && o.SetTagData(e as string))
                {
                    CanSave();
                };
            };

            olvColumnTags.Renderer = new HighlightTextRenderer() { UseGdiTextRendering = false, UseRoundedRectangle = false, FramePen = Pens.MediumPurple, FillBrush = Brushes.PaleVioletRed };
            filter = (olvColumnTags.Renderer as HighlightTextRenderer).Filter = new TextMatchFilter(olvTags);
            DateTime lastTime = default(DateTime);
            OLVListItem lastItem = null;
            olvTags.CellEditStarting += (s, e) =>
            {
                if (e.Column == olvColumnTags)
                {
                    if(lastItem != null)
                    {
                        if(DateTime.UtcNow.Subtract(lastTime).TotalSeconds <= 0.5f)
                            lastItem.Selected = false;
                        lastItem = null;
                    }
                    e.ListViewItem.Selected = true;
                    if (e.Control is TextBox tb)
                    {
                        if(settings.PreviewSize >= 64)
                        {
                            tb.Multiline = true;
                            tb.ScrollBars = ScrollBars.Vertical;
                        }
                        tb.Bounds = e.CellBounds;
                        tb.Location = new Point(e.CellBounds.Location.X, e.CellBounds.Location .Y + ((e.CellBounds.Height - tb.Height) / 2));
                    }
                }
            };

            olvTags.CellEditFinished += (s, e) =>
            {
                lastItem = e.ListViewItem;
                lastTime = DateTime.UtcNow;
                UpdateSelection();
            };

            SetImagePreviews(settings.DisplayImageThumbnails, settings.PreviewSize);

            var cliptypes = EnumUtil.GetNames<ClipType>();
            kryptonCMBClipType.Items.AddRange(cliptypes);
            clipType = settings.ClipType;
            kryptonCMBClipType.SelectedItem = EnumUtil.GetName(clipType);
            kryptonCMBClipType.SelectedIndexChanged += (s, e) =>
            {
                clipType = EnumUtil.ParseOrDefault<ClipType>(kryptonCMBClipType.SelectedItem as string);
            }; 

            KeyDown += (s, e) =>
            {

                if (e.KeyCode == Keys.PageUp || e.KeyCode == Keys.PageDown)
                {
                    int totalItems = olvTags.Items.Count;
                    if (totalItems <= 1)
                        return;

                    bool pageUp = e.KeyCode == Keys.PageUp;
                    int selectedIndex = olvTags.SelectedIndex;
                    int currentIndex = selectedIndex == -1 ? (pageUp ? totalItems = 1 : 0) : selectedIndex;

                    int newIndex = currentIndex + (selectedIndex == -1 ? 0 : (pageUp ? -1 : 1));
                    if (newIndex < 0)
                        newIndex = totalItems - 1;
                    else if (newIndex > totalItems -1)
                        newIndex = 0;

                    SetSelectedIndex(newIndex, true);
                    e.Handled = true;
                }
            };

            BusyChanged += (s, e) =>
            {
                settingsForm.BusyChanged(e.Busy);
            };

            FormClosing += (s, e) =>
            {
                if (e.CloseReason == CloseReason.UserClosing)
                {
                    if (Busy || (hasDirtyState && ConfirmationDialog.ShowDialogIf(this, ref settings.quitConfirmation, "You have unsaved changes, are you sure you want to quit?") != DialogResult.Yes))
                    {
                        e.Cancel = true;
                    }
                    else
                    {
                        settings.ClipType = clipType;
                        settings.FitSize = imageControl1.SizeMode == StableDiffusionTools.Controls.ImageSizeMode.Fit;
                        settings.LastZoom = imageControl1.Zoom;
                        if (SizeUtil.TryParse(kryptonCMBCropSize.Text, out Size letterboxSelection))
                            settings.LastLetterboxSelection = letterboxSelection;

                        if (SizeUtil.TryParse(kryptonCMBFinalSize.Text, out Size finalSizeSelection))
                            settings.LastFinalSizeSelection = finalSizeSelection;

                        settings.LastSidePanelWidth = kryptonSplitContainer1.ClientSize.Width - kryptonSplitContainer1.Panel1.ClientSize.Width;
                        settings.LastSidePanelHeight = kryptonSplitContainer2.ClientSize.Height - kryptonSplitContainer2.Panel1.ClientSize.Height;

                        settingsMan.SaveSettings(settings);
                        closing = true;

                        if (!tagSearchReplaceForm.Disposing && !tagSearchReplaceForm.IsDisposed)
                        {
                            tagSearchReplaceForm.Close();
                        }

                        if (!settingsForm.Disposing && !settingsForm.IsDisposed)
                        {
                            settingsForm.Close();
                        }

                        Application.RemoveMessageFilter(this);
                    }
                }
            };

            if (settings.LastSidePanelWidth != -1)
                kryptonSplitContainer1.SplitterDistance = kryptonSplitContainer1.ClientSize.Width - settings.LastSidePanelWidth;
            if (settings.LastSidePanelHeight != -1)
                kryptonSplitContainer2.SplitterDistance = kryptonSplitContainer2.ClientSize.Height - settings.LastSidePanelHeight;

            tagSearchReplaceForm = new TagSearchReplaceForm(settings);
            tagSearchReplaceForm.MarkedEvent += 
                (s, e) => SetFilter(e.StringFilter, e.RegexFilter);

            tagSearchReplaceForm.UnmarkedEvent += 
                (s, e) => ClearFilter();

            tagSearchReplaceForm.ReplaceEvent += (s, e) =>
            {
                if (Busy)
                    return;

                int matchedTags = 0;
                var items = e.Selected ? tolvTags.SelectedObjects : tolvTags.Objects;
                List<DataTags> updated = new List<DataTags>();
                foreach (var tag in items)
                {
                    if (tag.TagData.Contains(e.From))
                    {
                        string newTagData = tag.TagData.Replace(e.From, e.To);
                        if (tag.SetTagData(newTagData))
                        {
                            updated.Add(tag);
                            matchedTags++;
                        }
                    }
                }
                olvTags.BeginUpdate();
                olvTags.RefreshObjects(updated);
                olvTags.EndUpdate();

                if (updated.Count > 0)
                    CanSave();

                e.TagsCount = items.Count;
                e.TagsMatched = matchedTags;
            };

            tagSearchReplaceForm.RequestTagsFromParent += 
                (s, e) => e.Tags = e.Selected ? tolvTags.SelectedObjects : tolvTags.Objects;

            tagSearchReplaceForm.FormClosing += (s, e) =>
            {
                if (!closing && e.CloseReason == CloseReason.UserClosing)
                {
                    e.Cancel = true;
                    tagSearchReplaceForm.Hide();
                }
            };

            settingsForm = new SettingsForm(settings, this);
            settingsForm.FormClosing += (s, e) =>
            {
                if (!closing && e.CloseReason == CloseReason.UserClosing)
                {
                    e.Cancel = true;
                    settingsForm.Hide();
                }
            };
        }

        private async void tstbPath_KeyDownAsync(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Enter)
            {
                if (ExtendedPathInfo.GetPathInfo(tstbPath.Text) == PathInfo.Directory)
                {
                    e.SuppressKeyPress = true;
                    await PopulateTagsFromPathAsync(tstbPath.Text);
                }
            }
        }

        private void tsbBrowse_MouseUp(object sender, MouseEventArgs e)
        {
            var folder = new FolderSelectDialog();
            if (!string.IsNullOrEmpty(settings.LastPath))
                folder.InitialDirectory = settings.LastPath;

            if (folder.ShowDialog() && !IsDisposed)
            {
                settings.LastPath = tstbPath.Text = folder.FileName;
            }
        }

        private async void tsbLoad_MouseUp(object sender, MouseEventArgs e)
        {
            if (ExtendedPathInfo.GetPathInfo(tstbPath.Text) == PathInfo.Directory)
            {
                await PopulateTagsFromPathAsync(tstbPath.Text);
            }
        }

        private async void tsbSave_MouseUp(object sender, MouseEventArgs e)
        {
            if (ConfirmationDialog.ShowDialogIf(this, ref settings.saveConfirmation, "You are about to save these changes, this can't be undone, are you sure?") == DialogResult.Yes)
            {
                int tagChanged = 0;
                int cropped = 0;
                int changed = 0;
                SetStatusStripStatus("Saving...");
                tspbProgressEx.Value = 0;
                SetEnabledState(false);
                List<DataTags> updated = new List<DataTags>();
                var items = tolvTags.Objects;
                ThrottleProgress<CountProgress> progress = new ThrottleProgress<CountProgress>(TimeSpan.FromSeconds(0.5));
                void progressChanged(object s, CountProgress p)
                {
                    tspbProgressEx.Value = p.GetProgressInt();
                }
                progress.ProgressChanged += progressChanged;
                await Task.Run(()=> 
                {
                    for (int i = 0; i < items.Count; i++)
                    {
                        ((IProgress<CountProgress>)progress).Report(new CountProgress(items.Count, i + 1));
                        DataTags item = items[i];
                        bool tagDataSaved = item.SaveTagData();
                        bool imageCropped = item.Crop();
                        if (tagDataSaved || imageCropped)
                        {
                            if (tagDataSaved)
                                tagChanged++;

                            if (imageCropped)
                                cropped++;

                            changed++;
                            updated.Add(item);
                        }
                    }
                });

                progress.ProgressChanged -= progressChanged;
                olvTags.BeginUpdate();
                olvTags.RefreshObjects(updated);
                olvTags.EndUpdate();
                hasDirtyState = false;
                SetEnabledState(true);
                SetStatusStripStatus($"Saved {changed} changed files (Tags: {tagChanged} Cropped: {cropped})");
                tspbProgressEx.Value = 100;
            }
        }

        private void countTagsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            tolvTags.SelectedObject = null;
            kryptonTBEditTags.Text = CountTags(tolvTags.Objects, MathUtil.Clamp(Environment.ProcessorCount - 1, 1, settings.MaxWorkerThreads));
        }

        private void tsbSearchAndReplace_MouseUp(object sender, MouseEventArgs e)
        {
            if (tagSearchReplaceForm.Visible)
            {
                if (tagSearchReplaceForm.WindowState == FormWindowState.Minimized)
                    tagSearchReplaceForm.WindowState = FormWindowState.Normal;

                tagSearchReplaceForm.Activate();
            }
            else
            {
                tagSearchReplaceForm.Show(this);
            }
        }

        private void tsbEditTags_MouseUp(object sender, MouseEventArgs e)
        {
            OpenTagEditor(tolvTags.Objects);
            UpdateSelection();
        }

        private void clearToolStripMenuItem_Click(object sender, EventArgs e)
        {
            ClearFilter();
        }

        private void markToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var oldMarks = filter.ContainsStrings.Where((f) => !string.IsNullOrEmpty(f)).ToList();
            using (var marks = new MultiLineInputDialog("Input one filter per line to mark", "Marking", oldMarks, true))
            {
                if (marks.ShowDialog() == DialogResult.OK)
                {
                    SetFilter(filter.ContainsStrings = marks.Result, new string[0]);
                }
            }
        }

        private void tsbSettings_MouseUp(object sender, MouseEventArgs e)
        {
            if (settingsForm.Visible)
            {
                settingsForm.Activate();
            }
            else
            {
                settingsForm.Show(this);
            }
        }

        private void imageControl1_MouseEnter(object sender, EventArgs e)
        {
            kryptonCHKBTNLetterbox.Visible = true;
            UpdateCroppingControlsVisiblity();
        }

        private void kryptonCHKBTNLetterbox_CheckedChanged(object sender, EventArgs e)
        {
            imageControl1.LetterboxEnabled = kryptonCHKBTNLetterbox.Checked;
            UpdateCroppingControlsVisiblity();
        }

        private void kryptonCHKBTNFinalSize_CheckedChanged(object sender, EventArgs e)
        {
            kryptonCMBFinalSize.Visible = kryptonCHKBTNFinalSize.Checked && kryptonCHKBTNLetterbox.Visible && kryptonCHKBTNLetterbox.Checked;
            kryptonCMBFinalSize.BringToFront();
        }

        private void olvTags_SelectedIndexChanged(object sender, EventArgs e)
        {
            UpdateSelection();
        }

        private void kryptonTBEditTags_KeyDown(object sender, KeyEventArgs e)
        {
           if (e.Shift || e.KeyCode != Keys.Enter)
                return;

            e.SuppressKeyPress = true;

            if (!Busy)
            {
                IList<DataTags> tags = tolvTags.SelectedObjects;
                if (tags != null && tags.Count > 0)
                {
                    List<DataTags> updated = new List<DataTags>();
                    for (int i = 0; i < tags.Count; i++)
                    {
                        if (tags[i].SetTagData(kryptonTBEditTags.Text))
                        {
                            updated.Add(tags[i]);
                        }
                    }
                    olvTags.BeginUpdate();
                    olvTags.RefreshObjects(updated);
                    olvTags.EndUpdate();
                    if (updated.Count > 0)
                        CanSave();
                }
            }
        }

        private void kryptonTBEditTags_DragEnter(object sender, DragEventArgs e)
        {
            if (!Busy && e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effect = DragDropEffects.Copy;
            }
            else
            {
                e.Effect = DragDropEffects.None;
            }
        }

        private async void kryptonTBEditTags_DragDrop(object sender, DragEventArgs e)
        {
            if (!Busy && e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                SetEnabledState(false);

                string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
                List<string> dirs = new List<string>();
                for (int i = 0; i < files.Length; i++)
                {
                    if (ExtendedPathInfo.GetPathInfo(files[i]) == PathInfo.Directory)
                    {
                        dirs.Add(files[i]);
                    }
                }

                int procCount = Environment.ProcessorCount;
                int taskThreads = MathUtil.Clamp(procCount / 2, 1, settings.MaxFileReadThreads);
                int subTaskThreads = MathUtil.Clamp(procCount / taskThreads, 1, settings.MaxWorkerThreads);

#if DEBUG
                System.Diagnostics.Debug.WriteLine($"Processor Count: {procCount} - Per Task: {taskThreads} - Per SubTask: {subTaskThreads}");
#endif

                await Task.Run(() =>
                {
                    int counter = 0;
                    object locker = new object();
                    Parallel.ForEach(dirs, new ParallelOptions() { MaxDegreeOfParallelism = taskThreads },
                        (t) =>
                        {
                            string txtName = Path.GetFileNameWithoutExtension(t) + ".txt";
                            DirectoryInfo parentDir = Directory.GetParent(t);
                            string newPath = Path.Combine(parentDir.FullName, txtName);
                            Tuple<List<string>, List<string>> paths = LoadTextAndImagePathsFromPath(t, false);
                            List<DataTags> tags = LoadTagsParallel(paths.Item1, null, null, dirs.Count == 1 ? taskThreads : 1);
                            if (tags.Count > 0)
                            {
                                lock (locker)
                                {
                                    Invoke((MethodInvoker)delegate
                                    {
                                        SetStatusStripStatus($"Processing... {counter + 1}/{dirs.Count} tagged directories");
                                        tspbProgressEx.Value = (int)Math.Floor((float)counter / dirs.Count * 100);
                                    });
                                    counter++;
                                }

                                string counted = CountTags(tags, subTaskThreads);
                                File.WriteAllText(newPath, counted);
                            }
                        }
                    );
                });
                SetStatusStripStatus($"{dirs.Count} directories processed");
                tspbProgressEx.Value = 100;
                SetEnabledState(true);
            }
        }

        private void olvTags_DragEnter(object sender, DragEventArgs e)
        {
            if (!Busy && e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effect = DragDropEffects.Copy;
            }
            else
            {
                e.Effect = DragDropEffects.None;
            }
        }

        private async void olvTags_DragDrop(object sender, DragEventArgs e)
        {
            if (!Busy && e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
                if (files.Length == 1 && ExtendedPathInfo.GetPathInfo(files[0]) == PathInfo.Directory)
                {
                    tstbPath.Text = files[0];
                    await PopulateTagsFromPathAsync(tstbPath.Text);
                }
            }
        }

        private void olvTags_KeyDown(object sender, KeyEventArgs e)
        {
            if (olvTags.IsCellEditing)
                return;

            if (e.KeyCode == Keys.Escape)
            {
                olvTags.DeselectAll();
                e.SuppressKeyPress = true;
            }
            else if(!Busy)
            {
                if (e.Control)
                {
                    if (e.KeyCode == Keys.C)
                    {
                        e.SuppressKeyPress = true;
                        if (tolvTags.SelectedObject != null)
                        {
                            ClipboardUtil.SetText(tolvTags.SelectedObject.TagData);
                        }
                    }
                    else if (e.KeyCode == Keys.V)
                    {
                        e.SuppressKeyPress = true;
                        if (Clipboard.ContainsText())
                        {
                            string text = Clipboard.GetText();
                            var selectedItems = tolvTags.SelectedObjects;
                            if (selectedItems.Count > 0)
                            {
                                List<DataTags> updated = new List<DataTags>();
                                foreach (var item in selectedItems)
                                {
                                    if (item.SetTagData(text))
                                    {
                                        updated.Add(item);
                                    }
                                }
                                olvTags.BeginUpdate();
                                olvTags.RefreshObjects(updated);
                                olvTags.EndUpdate();

                                if (updated.Count > 0)
                                    CanSave();
                            }
                        }
                    }
                }
            }
        }

        private void contextMenuStrip1_Opening(object sender, System.ComponentModel.CancelEventArgs e)
        {
            bool hasSingleSelectedItem = tolvTags.SelectedObject != null;
            bool hasMultipleSelectedItems = olvTags.SelectedIndices.Count > 1;

            openToolStripMenuItem.Enabled = hasSingleSelectedItem;
            openExplorerToolStripMenuItem.Enabled = hasSingleSelectedItem;
            openTagEditorToolStripMenuItem.Enabled = !Busy && (hasSingleSelectedItem || hasMultipleSelectedItems);
            interrogateToolStripMenuItem.Enabled = (interrogator.Busy || !Busy) && (hasSingleSelectedItem || hasMultipleSelectedItems);
        }

        private void openToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if(tolvTags.SelectedObject != null)
            {
                if(File.Exists(tolvTags.SelectedObject.FullPath))
                    ProcessUtil.LaunchAsync(tolvTags.SelectedObject.FullPath);
                else if(!string.IsNullOrEmpty(tolvTags.SelectedObject.ImagePath) && File.Exists(tolvTags.SelectedObject.ImagePath))
                {
                    ProcessUtil.LaunchAsync(tolvTags.SelectedObject.ImagePath);
                }
            }
        }

        private void openExplorerToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (tolvTags.SelectedObject != null)
            {
                if (File.Exists(tolvTags.SelectedObject.FullPath))
                    ExplorerUtil.Open(tolvTags.SelectedObject.FullPath, !ModifierKeys.HasFlag(Keys.Shift));
                else if (!string.IsNullOrEmpty(tolvTags.SelectedObject.ImagePath) && File.Exists(tolvTags.SelectedObject.ImagePath))
                {
                    ExplorerUtil.Open(tolvTags.SelectedObject.ImagePath, !ModifierKeys.HasFlag(Keys.Shift));
                }
            }
        }

        private void openTagEditorToolStripMenuItem_Click(object sender, EventArgs e)
        {
            OpenTagEditor(tolvTags.SelectedObjects);
            UpdateSelection();
        }

        private async void interrogateToolStripMenuItem_Click(object sender, EventArgs e)
        {
            bool gotSettings = settingsForm.FetchedSettings || await settingsForm.LoadOptions();
            if (!gotSettings)
            {
                SetStatusStripStatus("Failed to fetch settings from WebUI API", warning: true);
                return;
            }

            List<DataTags> enqueue = new List<DataTags>(tolvTags.SelectedObjects.Where((t) => t.ScheduledForInterrogation == false));
            if(enqueue.Count > 0)
            {
                if(interrogator.ScheduleForInterrogation(enqueue, clipType))
                {
                    olvTags.Invalidate();

                    if (Busy)
                        return;

                    SetStatusStripStatus("Running Interrogator");
                    SetEnabledState(false);
                }
            }
        }

        private void OpenTagEditor(IList<DataTags> tagList)
        {
            var tagEditorForm = new TagEditorForm();
            var tags = GetTagCounts(tagList, out int maxLen, MathUtil.Clamp(Environment.ProcessorCount - 1, 1, settings.MaxWorkerThreads));
            if (tagEditorForm.ShowDialog(this, tags, tagList.Count) == DialogResult.OK)
            {
                var changedTags = tagEditorForm.ResultTags.Where((t) => t.Changed);
                string[] removed = changedTags.Where(t => t.Action == TagEditAction.Remove).Select(t => t.Name).ToArray();
                string[] addedFront = changedTags.Where(t => t.Action == TagEditAction.AddFront).Select(t => t.Name).ToArray();
                string[] addedBack = changedTags.Where(t => t.Action == TagEditAction.AddBack).Select(t => t.Name).ToArray();
                string[][] renamed = changedTags.Where(t => t.Action == TagEditAction.Rename).Select(t => new string[] { t.Name, t.NewName }).ToArray();
                List<DataTags> updated = new List<DataTags>();
                foreach (var item in tagList)
                {
                    if (item.ApplyActions(removed, addedFront, addedBack, renamed))
                    {
                        updated.Add(item);
                    }
                }
                olvTags.RefreshObjects(updated);
                SetStatusStripStatus($"Updated {updated.Count} Tag files");
                if (updated.Count > 0)
                    CanSave();
            }
        }

        private async Task<bool> PopulateTagsFromPathAsync(string path)
        {
            if (Busy || (hasDirtyState && ConfirmationDialog.ShowDialogIf(this, ref settings.loadConfirmation, "You have unsaved changes, are you sure you want to load this path?") != DialogResult.Yes))
                return false;

            if(Uri.TryCreate(path, UriKind.Absolute, out Uri uri))
            {
                path = uri.LocalPath.TrimEnd('\\');
            }

            settings.LastPath = path;
            hasDirtyState = false;
            SetEnabledState(false);
            Progress<CountProgress> progress = new Progress<CountProgress>();
            void progressChanged(object s, CountProgress c)
            {
                SetStatusStripStatus($"Loading... {c.Completed} Tag files found");
                tspbProgressEx.Value = c.GetProgressInt();
            }
            progress.ProgressChanged += progressChanged;

            int procCount = Environment.ProcessorCount;
            int taskThreads = MathUtil.Clamp(procCount / 2, 1, settings.MaxFileReadThreads);
            Tuple<List<string>, List<string>> paths = LoadTextAndImagePathsFromPath(tstbPath.Text);
            List<DataTags> tags = await Task.Run(() => LoadTagsParallel(paths.Item1, path, progress, taskThreads));
            if(paths.Item2.Count > 0)
            {
                if(ConfirmationDialog.ShowDialogIf(this, ref settings.createConfirmation, $"There are {paths.Item2.Count} image(s) that appear to be missing tag files, do you want to create these now?") == DialogResult.Yes)
                {
                    List<DataTags> tagsMissing = await Task.Run(() => 
                    {
                        List<DataTags> result = new List<DataTags>();
                        result.AddRange(tags);
                        for (int i = 0; i < paths.Item2.Count; i++)
                        {
                            string ext = Path.GetExtension(paths.Item2[i]);
                            result.Add(new DataTags($"{paths.Item2[i].Substring(0, paths.Item2[i].Length - ext.Length)}.txt", path, paths.Item2[i], true));
                        }
                        return result.OrderBy((t) => t.FullPath, new CustomComparer<string>(CompareUtil.CompareNatural)).ToList();
                    });
                    tags = tagsMissing;
                    hasDirtyState = true;
                }
            }
            olvTags.BeginUpdate();
            olvTags.ClearObjects();
            ClearSelection();
            imageListLarge.Images.Clear();
            imageListSmall.Images.Clear();
            thumbnailLoader.Clear();
            olvTags.AddObjects(tags);
            olvTags.EndUpdate();
            progress.ProgressChanged -= progressChanged;
            SetStatusStripStatus($"Loaded {tags.Count} Tag files");
            if (displayImagePreviews && settings.PrecacheImageThumbnails)
            {
                var precache = tags.Where((t) => !string.IsNullOrEmpty(t.ImagePath)).ToList();
                foreach (var item in precache)
                {
                    if (!thumbnailLoader.HasBeenScheduled(item))
                        thumbnailLoader.ScheduleForLoad(item, imageListSmall.ImageSize);
                }
            }



            tspbProgressEx.Value = 100;
            SetEnabledState(true);

            return true;
        }

        private Tuple<List<string>, List<string>> LoadTextAndImagePathsFromPath(string path, bool searchForMissingDataTags = true)
        {
            Queue<string> dirs = new Queue<string>(new string[] { path });
            List<string> txtFiles = new List<string>();
            List<string> imgFiles = new List<string>();
            while (dirs.Count > 0)
            {
                string item = dirs.Dequeue();
                var info = ExtendedPathInfo.GetPathInfo(item);
                if (info == PathInfo.Directory)
                {
                    string[] itemDirs = Directory.GetDirectories(item);
                    for (int i = 0; i < itemDirs.Length; i++)
                    {
                        dirs.Enqueue(itemDirs[i]);
                    }
                }
                string[] files = Directory.GetFiles(item);
                List<string> foundTextFiles = new List<string>();
                List<string> foundImageFiles = new List<string>();
                for (int i = 0; i < files.Length; i++)
                {
                    string ext = Path.GetExtension(files[i]);
                    if (ext.Equals(".txt", StringComparison.InvariantCultureIgnoreCase))
                    {
                        foundTextFiles.Add(files[i]);
                        txtFiles.Add(files[i]);
                    }
                    else if(searchForMissingDataTags)
                    {
                        for (int j = 0; j < ImageUtil.CommonImageExtensions.Count; j++)
                        {
                            if (ImageUtil.CommonImageExtensions[j].Equals(ext, StringComparison.InvariantCultureIgnoreCase))
                            {
                                foundImageFiles.Add(files[i]);
                                break;
                            }
                        }
                    }
                }
                if (searchForMissingDataTags)
                {
                    for (int i = 0; i < foundImageFiles.Count; i++)
                    {
                        bool foundMatch = false;
                        string imageName = Path.GetFileNameWithoutExtension(foundImageFiles[i]);
                        string rootPath = Directory.GetParent(foundImageFiles[i]).FullName;
                        for (int j = 0; j < foundTextFiles.Count; j++)
                        {
                            string txtName = Path.GetFileNameWithoutExtension(foundTextFiles[j]);
                            if (txtName.Equals(imageName, StringComparison.InvariantCultureIgnoreCase))
                            {
                                foundMatch = true;
                                break;
                            }
                        }
                        if (!foundMatch)
                        {
                            imgFiles.Add(foundImageFiles[i]);
                        }
                    }
                }
            }

            return new Tuple<List<string>, List<string>>(txtFiles, imgFiles);
        }

        private List<DataTags> LoadTagsParallel(List<string> txtFiles, string root, IProgress<CountProgress> progress = null, int maxDegreeOfParallelism = 1)
        {
            object locker = new object();
            int counter = 0;
            ConcurrentBag<DataTags> tags = new ConcurrentBag<DataTags>();

            Parallel.ForEach(txtFiles, new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, (t) =>
            {
                tags.Add(new DataTags(t, root));
                if (progress != null)
                {
                    int currentCount = Interlocked.Increment(ref counter);
                    if (currentCount % 100 == 0)
                    {
                        lock (locker)
                        {
                            progress.Report(new CountProgress(txtFiles.Count, currentCount));
                        }
                    }
                }
            });

            return tags.OrderBy((t) => t.FullPath, new CustomComparer<string>(CompareUtil.CompareNatural)).ToList();
        }


        private ConcurrentDictionary<string, int> GetTagCounts(IList<DataTags> inputTags, out int maxLen, int maxDegreeOfParallelism = 4)
        {
            object locker = new object();
            ConcurrentDictionary<string, int> tags = new ConcurrentDictionary<string, int>();
            int overallMaxLen = 0;
            char[] splitArray = ClipSplit[(int)clipType];

            Parallel.ForEach(inputTags, new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism },
                (t) =>
                {
                    int maxLength = 0;
                    string[] splitTags = t.TagData.Split(splitArray, StringSplitOptions.RemoveEmptyEntries);
                    for (int j = 0; j < splitTags.Length; j++)
                    {
                        string sTag = splitTags[j].Trim();

                        if (sTag.Length > maxLength)
                            maxLength = sTag.Length;

                        tags.AddOrUpdate(sTag, 1, (k, v) => v + 1);
                    }

                    //Locking seems plenty fast, switching to atomic/non-blocking operations seems overkill.
                    //InterlockedUtil.CompareExchangeIfGreaterThan(ref overallMaxLen, maxLenght);
                    lock (locker)
                    {
                        if (maxLength > overallMaxLen)
                            overallMaxLen = maxLength;
                    }
                }
            );

            maxLen = overallMaxLen;
            return tags;
        }

        private string CountTags(IList<DataTags> inputTags, int maxDegreeOfParallelism = 4)
        {
            ConcurrentDictionary<string, int> tags = GetTagCounts(inputTags, out int maxLen, maxDegreeOfParallelism);
            var sortedDict = from entry in tags orderby entry.Value descending select entry;
            var keys2 = sortedDict.Select(x => $"{x.Key.PadRight(maxLen, ' ')} {x.Value}");
            return string.Join(Environment.NewLine, keys2);
        }

        private void SetEnabledState(bool enabled)
        {
            tsbBrowse.Enabled = enabled;
            tsbLoad.Enabled = enabled;
            tsbSave.Enabled = hasDirtyState && enabled;
            countTagsToolStripMenuItem.Enabled = enabled;
            tsbEditTags.Enabled = enabled;
            Busy = !enabled;
        }

        private void CanSave()
        {
            hasDirtyState = true;
            tsbSave.Enabled = !Busy;
        }

        private void SetFilter(IEnumerable<string> filtered, IEnumerable<string> regex)
        {
            filter.RegexStrings = regex;
            filter.ContainsStrings = filtered;
            olvTags.Invalidate();
        }

        private void ClearFilter()
        {
            SetFilter(new string[0], new string[0]);
        }

        private void ClearSelection()
        {
            olvTags.SelectedIndex = -1;
            imageControl1.Image = null;
            kryptonTBEditTags.Clear();
        }

        private void UpdateSelection()
        {
            didCrop = false;

            UpdateCroppingControlsVisiblity();

            var selectedItems = tolvTags.SelectedObjects;
            if (selectedItems.Count == 0)
            {
                kryptonTBEditTags.Clear();
                imageControl1.Image = null;
            }
            else if (selectedItems.Count == 1)
            {
                DataTags tag = selectedItems[0];
                SetTagEditFromTag(tag, true);
                if (!string.IsNullOrEmpty(tag.ImagePath))
                {
                    try
                    {
                        if (tag.ChangedCrop)
                        {
                            imageControl1.SetLetterBoxBounds(tag.CropRectangle, false);
                            SetSelectedCropSize(tag.CropRectangle.Size, true);
                        }

                        if (tag.ChangedFinalSize)
                        {
                            SetSelectedFinalSize(tag.FinalSize, true);
                        }

                        imageControl1.SetImage(ImageUtil.LoadBitmap(tag.ImagePath), true, tag.CropRectangle == Rectangle.Empty);
                    }
                    catch (Exception ex)
                    {
                        imageControl1.Image = null;
                    }
                }
                else
                {
                    imageControl1.Image = null;
                }
            }
            else
            {
                imageControl1.Image = null;

                bool allTagDataMatches = true;
                DataTags firstTag = null;
                for (int i = 0; i < selectedItems.Count; i++)
                {
                    if (firstTag == null)
                    {
                        firstTag = selectedItems[i];
                    }
                    else
                    {
                        if (!firstTag.TagDataMatches(selectedItems[i]))
                        {
                            allTagDataMatches = false;
                            break;
                        }
                    }
                }

                if (allTagDataMatches)
                {
                    SetTagEditFromTag(firstTag);
                }
                else
                {
                    kryptonTBEditTags.Clear();
                }
            }
        }

        private void UpdateCroppingControlsVisiblity()
        {
            if (kryptonCHKBTNLetterbox.Visible)
            {
                kryptonCMBCropSize.Visible = kryptonCHKBTNFinalSize.Visible = kryptonCHKBTNLetterbox.Checked;
                kryptonBTNApplyCrop.Visible = kryptonCHKBTNLetterbox.Checked && !didCrop;
                kryptonCMBFinalSize.Visible = kryptonCHKBTNFinalSize.Checked && kryptonCHKBTNLetterbox.Checked;

                kryptonBTNApplyCrop.BringToFront();
                kryptonCHKBTNLetterbox.BringToFront();
                kryptonCMBCropSize.BringToFront();
                kryptonCHKBTNFinalSize.BringToFront();
                kryptonCMBFinalSize.BringToFront();
            }
        }

        private void SetSelectedIndex(int newIndex, bool ensureVisible = false)
        {

            olvTags.SelectedIndex = newIndex;
            if (ensureVisible && newIndex >= 0 && newIndex < olvTags.Items.Count)
                olvTags.Items[newIndex].EnsureVisible();
        }

        private void SetTagEditFromTag(DataTags tag, bool forceRefresh = false)
        {
            kryptonTBEditTags.Text = tag.TagData;
            if(forceRefresh)
                kryptonTBEditTags.Refresh();
        }

        private bool TryGetSelectedCropSize(out Size size)
        {
            if(SizeUtil.TryParse(kryptonCMBCropSize.SelectedItem as string, out size) || SizeUtil.TryParse(kryptonCMBCropSize.Text, out size))
            {
                return true;
            }
            return false;
        }

        private bool TryGetSelectedFinalSize(out Size size)
        {
            if (SizeUtil.TryParse(kryptonCMBFinalSize.SelectedItem as string, out size) || SizeUtil.TryParse(kryptonCMBFinalSize.Text, out size))
            {
                return true;
            }
            return false;
        }

        private void SetSelectedCropSize(Size size, bool ignoreIndexChangedEvent = false)
        {
            string cropSize = $"{size.Width}x{size.Height}";
            int cropIndex = kryptonCMBCropSize.Items.IndexOf(cropSize);
            if (cropIndex != -1)
            {
                if(ignoreIndexChangedEvent)
                    ignoreCropIndexChanged = true;

                kryptonCMBCropSize.SelectedIndex = cropIndex;

                if (ignoreIndexChangedEvent)
                    ignoreCropIndexChanged = false;
            }
            else
            {
                kryptonCMBCropSize.Text = cropSize;
            }
        }

        private void SetSelectedFinalSize(Size size, bool ignoreIndexChangedEvent = false)
        {
            string finalSize = $"{size.Width}x{size.Height}";
            int resizeIndex = kryptonCMBFinalSize.Items.IndexOf(finalSize);
            if (resizeIndex != -1)
            {
                if (ignoreIndexChangedEvent)
                    ignoreFinalIndexChanged = true;

                kryptonCMBFinalSize.SelectedIndex = resizeIndex;

                if (ignoreIndexChangedEvent)
                    ignoreFinalIndexChanged = false;
            }
            else
            {
                kryptonCMBFinalSize.Text = finalSize;
            }
        }

        private bool CropSelectedItem()
        {
            bool result = false;
            DataTags selected = tolvTags.SelectedObject;
            bool doCrop = kryptonCHKBTNLetterbox.Checked;
            bool doResize = kryptonCHKBTNFinalSize.Checked;

            if (didCrop)
            {
                SetStatusStripStatus($"Cropping a already cropped preview is not supported at this time", "reload the preview", warning: true);
                return false;
            }

            if (selected != null && !string.IsNullOrEmpty(selected.ImagePath) && doCrop)
            {
                var cropRect = Rectangle.Truncate(imageControl1.LetterboxBounds);
                Rectangle bounds = cropRect;
                bounds.Intersect(new Rectangle(new Point(), imageControl1.ImageSize));

                bool aspectRatioGuided = false;
                float destAspect = 0f;
                float sourceAspect = 0f;
                Size resizeSize = default(Size);

                Rectangle destBounds;
                if (!doResize)
                {
                    destBounds = new Rectangle(0, 0, bounds.Width, bounds.Height);
                }
                else
                {
                    if (TryGetSelectedFinalSize(out resizeSize))
                    {
                        sourceAspect = (float)bounds.Width / bounds.Height;
                        destAspect = (float)resizeSize.Width / resizeSize.Height;

                        if (MathUtil.NearEqual(sourceAspect, destAspect))
                        {
                            destBounds = new Rectangle(0, 0, resizeSize.Width, resizeSize.Height);
                        }
                        else
                        {
                            double ratioX = resizeSize.Width / (double)bounds.Width;
                            double ratioY = resizeSize.Height / (double)bounds.Height;
                            double ratio = ratioX < ratioY ? ratioX : ratioY;
                            destBounds = new Rectangle(0, 0, (int)(bounds.Width * ratio), (int)(bounds.Height * ratio));
                            aspectRatioGuided = true;
                        }

                        if (bounds.Size == destBounds.Size)
                            doResize = false;
                    }
                    else
                    {
                        SetStatusStripStatus($"Failed to parse resize selection, skipping...");
                        return false;
                    }
                }

                bool changedCrop = selected.ChangedCrop;
                bool changedFinalSize = selected.ChangedFinalSize;

                if (bounds.X == 0 && bounds.Y == 0 && bounds.Size == imageControl1.ImageSize && bounds.Size == destBounds.Size)
                {
                    if(changedCrop || changedFinalSize)
                    {
                        selected.CropRectangle = Rectangle.Empty;
                        selected.FinalSize = Size.Empty;
                        olvTags.RefreshObject(selected);
                        SetStatusStripStatus($"Crop is the same as source, resetting crop...");
                    }
                    else
                    {
                        SetStatusStripStatus($"Crop is the same as source, skipping...");
                    }

                    return false;
                }

                changedCrop = selected.CropRectangle != cropRect;
                changedFinalSize = selected.FinalSize != destBounds.Size;
                if (!changedCrop && !changedFinalSize)
                {
                    SetStatusStripStatus($"Crop is the same as existing, skipping...");
                    return false;
                }

                didCrop = true;
                kryptonBTNApplyCrop.Visible = false;
                selected.CropRectangle = (cropRect.X != 0 || cropRect.Y != 0 || cropRect.Size != imageControl1.ImageSize) ? cropRect : Rectangle.Empty;
                selected.FinalSize = doResize ? resizeSize : Size.Empty; 
                CanSave();

                if (doResize && (bounds.Size != destBounds.Size))
                {
                    if (aspectRatioGuided)
                    {
                        var gcdSrc = MathUtil.GCD(bounds.Width, bounds.Height);
                        var gcdDest = MathUtil.GCD(resizeSize.Width, resizeSize.Height);
                        SetStatusStripStatus($"Cropped: [{bounds.X}, {bounds.Y} - {bounds.Width}x{bounds.Height}] -> [{destBounds.Width}x{destBounds.Height}]", $"(aspect ratio missmatch {bounds.Width / gcdSrc}:{bounds.Height / gcdSrc} -> {resizeSize.Width / gcdDest}:{resizeSize.Height / gcdDest})", warning: true);
                    }
                    else
                        SetStatusStripStatus(tsslStatus.Text = $"Cropped: [{bounds.X}, {bounds.Y} - {bounds.Width}x{bounds.Height}] -> [{destBounds.Width}x{destBounds.Height}]");

                }
                else
                {
                    SetStatusStripStatus($"Cropped: [{bounds.X}, {bounds.Y} - {bounds.Width}x{bounds.Height}]");
                }

                
                using (var outputCropped = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppPArgb))
                {
                    Bitmap resizedBitmap = null;
                    try
                    {
                        using (var fastOutputCropped = outputCropped.FastLock())
                        {
                            fastOutputCropped.CopyRegion(imageControl1.Image, bounds, new Rectangle(0, 0, bounds.Width, bounds.Height));
                        }

                        if (doResize)
                        {
                            resizedBitmap = BitmapResizer.ResizeBitmap(outputCropped, new BitmapResizeLanczosSettings(lanczocRadius, false, destBounds.Size));
                            imageControl1.SetImage(resizedBitmap, true, true);
                        }
                        else
                        {
                            imageControl1.SetImage(outputCropped, true, true);
                        }
#if false
                        using (var backupFile = File.Open(IOUtil.CreateBackupFilePath(selected.ImagePath), FileMode.CreateNew, FileAccess.Write, FileShare.None))
                        using (var imageStream = File.Open(selected.ImagePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
                        {
                            imageStream.CopyTo(backupFile, 1024 * 1024);
                            backupFile.Flush(true);
                            imageStream.Seek(0, SeekOrigin.Begin);
                            (doResize ? resizedBitmap : outputCropped).Save(imageStream, ImageUtil.GetImageFormatFromExtension(Path.GetExtension(selected.ImagePath)) ?? imageControl1.Image.RawFormat);
                            imageStream.SetLength(imageStream.Position);
                        }
#endif
                        imageListSmall.Images.RemoveByKey(selected.ImagePath);
                        using (var thumb = BitmapResizer.ResizeBitmap(outputCropped, new BitmapResizeGDISettings(GDIResizePresent.HighSpeed, imageListSmall.ImageSize) { AddPadding = true, PaddingColor = Color.Transparent }))
                        {
                            imageListSmall.Images.Add(selected.ImagePath, thumb);
                        }
                        olvTags.RefreshObject(selected);

                        result = true;
                    }
                    catch (Exception ex)
                    {
                        KryptonMessageBox.Show(this, ExceptionUtil.FormatInnerExceptions(ex), "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                        SetStatusStripStatus($"Error: {ex.Message}", warning: true);
                    }
                    finally
                    {
                        if (resizedBitmap != null)
                            resizedBitmap.Dispose();
                    }
                }
            }
            return result;
        }

        private void SetStatusStripStatus(string status, string extra = "", bool warning = false)
        {
            tsslStatus.Text = status;
            tsslStatusWarn.Text = extra;
            tsslStatusWarn.Image = warning ? tsslStatusWarn.Image = StableDiffusionTools.Properties.Resources.error16 : null;
        }

        public void SetImagePreviews(bool enabled, int previewSize)
        {
            displayImagePreviews = false;

            imageListSmall.ImageSize = new Size(previewSize, previewSize);
            SetAutoFitFileColumn(enabled, previewSize, settings.FitFileColumnToThumbnails);
            (olvColumnFile.Renderer as BaseRenderer).CanWrap = previewSize >= 64;
            (olvColumnTags.Renderer as BaseRenderer).CanWrap = previewSize >= 64;
            displayImagePreviews = enabled;
            imageListSmall.Images.Clear();
            imageListLarge.Images.Clear();
            thumbnailLoader.Clear();

            olvTags.Invalidate();
        }

        public void SetAutoFitFileColumn(bool enabled, int previewSize, bool fitFileColumnToThumbnails)
        {
            olvColumnFile.Width = enabled && fitFileColumnToThumbnails ? previewSize : olvColumnFile.Width < 100 ? 100 : olvColumnFile.Width;
        }

        public void SetThumbnailLoaderMaxThreadCount(int maxThreads)
        {
            thumbnailLoader.MaxThreads = MathUtil.Clamp(Environment.ProcessorCount / 2, 1, maxThreads);
        }

        public void SetInterrogatorMaxThreadCount(int maxThreads)
        {
            interrogator.MaxThreads = maxThreads;
        }

        public void SetImageControlLetterboxColor(Color letterboxColor)
        {
            imageControl1.LetterboxColor = letterboxColor;
        }

        public bool CloseFromTaskTray()
        {
            if (Disposing || IsDisposed || !Visible)
                return true;

            Close();
            return closing;
        }
    }
}
